resend-verification-email.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. 'use client';
  2. import { useState } from 'react';
  3. import { useTranslations } from 'next-intl';
  4. interface ResendVerificationEmailProps {
  5. email: string;
  6. locale: string;
  7. }
  8. export default function ResendVerificationEmail({ email, locale }: ResendVerificationEmailProps) {
  9. const [isLoading, setIsLoading] = useState(false);
  10. const [message, setMessage] = useState('');
  11. const [messageType, setMessageType] = useState<'success' | 'error' | ''>('');
  12. const [cooldownRemaining, setCooldownRemaining] = useState(0);
  13. const t = useTranslations('auth.verifyEmailSent');
  14. const tErrors = useTranslations('auth.errors');
  15. const handleResend = async () => {
  16. if (isLoading || cooldownRemaining > 0) return;
  17. setIsLoading(true);
  18. setMessage('');
  19. setMessageType('');
  20. try {
  21. const response = await fetch('/api/auth/resend-verification', {
  22. method: 'POST',
  23. headers: {
  24. 'Content-Type': 'application/json',
  25. },
  26. body: JSON.stringify({ email, locale }),
  27. });
  28. const data = await response.json();
  29. if (response.ok) {
  30. setMessage(data.message);
  31. setMessageType('success');
  32. // 启动冷却期倒计时
  33. if (data.cooldownMinutes) {
  34. setCooldownRemaining(data.cooldownMinutes * 60); // 转换为秒
  35. const countdown = setInterval(() => {
  36. setCooldownRemaining((prev) => {
  37. if (prev <= 1) {
  38. clearInterval(countdown);
  39. return 0;
  40. }
  41. return prev - 1;
  42. });
  43. }, 1000);
  44. }
  45. } else {
  46. if (response.status === 429) {
  47. // 冷却期错误
  48. setMessage(data.error);
  49. setMessageType('error');
  50. // 从错误消息中提取冷却时间(简化处理)
  51. setCooldownRemaining(5 * 60); // 5分钟
  52. } else if (response.status === 400 && data.error.includes('已经验证')) {
  53. setMessage(data.error);
  54. setMessageType('success');
  55. } else {
  56. setMessage(data.error || tErrors('networkError'));
  57. setMessageType('error');
  58. }
  59. }
  60. } catch (error) {
  61. setMessage(tErrors('networkError'));
  62. setMessageType('error');
  63. } finally {
  64. setIsLoading(false);
  65. }
  66. };
  67. const formatCooldownTime = (seconds: number): string => {
  68. const minutes = Math.floor(seconds / 60);
  69. const remainingSeconds = seconds % 60;
  70. return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
  71. };
  72. const isDisabled = isLoading || cooldownRemaining > 0;
  73. return (
  74. <div className="space-y-3">
  75. {message && (
  76. <div className={`p-3 rounded-lg text-sm ${
  77. messageType === 'success'
  78. ? 'bg-green-50 text-green-800 border border-green-200'
  79. : 'bg-red-50 text-red-800 border border-red-200'
  80. }`}>
  81. {message}
  82. </div>
  83. )}
  84. <div className="text-center">
  85. <button
  86. onClick={handleResend}
  87. disabled={isDisabled}
  88. className={`w-full py-2 px-4 rounded-md text-sm font-medium transition-colors ${
  89. isDisabled
  90. ? 'bg-gray-100 text-gray-400 cursor-not-allowed'
  91. : 'bg-green-600 hover:bg-green-700 text-white hover:shadow-md'
  92. }`}
  93. >
  94. {isLoading ? (
  95. <span className="flex items-center justify-center">
  96. <svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-current" fill="none" viewBox="0 0 24 24">
  97. <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" className="opacity-25"></circle>
  98. <path fill="currentColor" className="opacity-75" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
  99. </svg>
  100. {t('sending')}
  101. </span>
  102. ) : cooldownRemaining > 0 ? (
  103. t('resendCooldown', { time: formatCooldownTime(cooldownRemaining) })
  104. ) : (
  105. t('resendEmail')
  106. )}
  107. </button>
  108. </div>
  109. {cooldownRemaining > 0 && (
  110. <div className="text-xs text-gray-500 text-center">
  111. {t('cooldownMessage', { time: formatCooldownTime(cooldownRemaining) })}
  112. </div>
  113. )}
  114. </div>
  115. );
  116. }